/*
 *  Remodal - v1.1.0
 *  Responsive, lightweight, fast, synchronized with CSS animations, fully customizable modal window plugin with declarative configuration and hash tracking.
 *  http://vodkabears.github.io/remodal/
 *
 *  Made by Ilya Makarov
 *  Under MIT License
 */

!(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        define(['jquery'], function ($) {
            return factory(root, $);
        });
    } else if (typeof exports === 'object') {
        factory(root, require('jquery'));
    } else {
        factory(root, root.jQuery || root.Zepto);
    }
})(this, function (global, $) {

    'use strict';

    /**
     * Name of the plugin
     * @private
     * @const
     * @type {String}
     */
    var PLUGIN_NAME = 'remodal';

    /**
     * Namespace for CSS and events
     * @private
     * @const
     * @type {String}
     */
    var NAMESPACE = global.REMODAL_GLOBALS && global.REMODAL_GLOBALS.NAMESPACE || PLUGIN_NAME;

    /**
     * Animationstart event with vendor prefixes
     * @private
     * @const
     * @type {String}
     */
    var ANIMATIONSTART_EVENTS = $.map(
        ['animationstart', 'webkitAnimationStart', 'MSAnimationStart', 'oAnimationStart'],

        function (eventName) {
            return eventName + '.' + NAMESPACE;
        }
    ).join(' ');

    /**
     * Animationend event with vendor prefixes
     * @private
     * @const
     * @type {String}
     */
    var ANIMATIONEND_EVENTS = $.map(
        ['animationend', 'webkitAnimationEnd', 'MSAnimationEnd', 'oAnimationEnd'],

        function (eventName) {
            return eventName + '.' + NAMESPACE;
        }
    ).join(' ');

    /**
     * Default settings
     * @private
     * @const
     * @type {Object}
     */
    var DEFAULTS = $.extend({
        hashTracking: true,
        closeOnConfirm: true,
        closeOnCancel: true,
        closeOnEscape: true,
        closeOnOutsideClick: true,
        modifier: '',
        appendTo: null
    }, global.REMODAL_GLOBALS && global.REMODAL_GLOBALS.DEFAULTS);

    /**
     * States of the Remodal
     * @private
     * @const
     * @enum {String}
     */
    var STATES = {
        CLOSING: 'closing',
        CLOSED: 'closed',
        OPENING: 'opening',
        OPENED: 'opened'
    };

    /**
     * Reasons of the state change.
     * @private
     * @const
     * @enum {String}
     */
    var STATE_CHANGE_REASONS = {
        CONFIRMATION: 'confirmation',
        CANCELLATION: 'cancellation'
    };

    /**
     * Is animation supported?
     * @private
     * @const
     * @type {Boolean}
     */
    var IS_ANIMATION = (function () {
        var style = document.createElement('div').style;

        return style.animationName !== undefined ||
            style.WebkitAnimationName !== undefined ||
            style.MozAnimationName !== undefined ||
            style.msAnimationName !== undefined ||
            style.OAnimationName !== undefined;
    })();

    /**
     * Is iOS?
     * @private
     * @const
     * @type {Boolean}
     */
    var IS_IOS = /iPad|iPhone|iPod/.test(navigator.platform);

    /**
     * Current modal
     * @private
     * @type {Remodal}
     */
    var current;

    /**
     * Scrollbar position
     * @private
     * @type {Number}
     */
    var scrollTop;

    /**
     * Returns an animation duration
     * @private
     * @param {jQuery} $elem
     * @returns {Number}
     */
    function getAnimationDuration($elem) {
        if (
            IS_ANIMATION &&
            $elem.css('animation-name') === 'none' &&
            $elem.css('-webkit-animation-name') === 'none' &&
            $elem.css('-moz-animation-name') === 'none' &&
            $elem.css('-o-animation-name') === 'none' &&
            $elem.css('-ms-animation-name') === 'none'
        ) {
            return 0;
        }

        var duration = $elem.css('animation-duration') ||
            $elem.css('-webkit-animation-duration') ||
            $elem.css('-moz-animation-duration') ||
            $elem.css('-o-animation-duration') ||
            $elem.css('-ms-animation-duration') ||
            '0s';

        var delay = $elem.css('animation-delay') ||
            $elem.css('-webkit-animation-delay') ||
            $elem.css('-moz-animation-delay') ||
            $elem.css('-o-animation-delay') ||
            $elem.css('-ms-animation-delay') ||
            '0s';

        var iterationCount = $elem.css('animation-iteration-count') ||
            $elem.css('-webkit-animation-iteration-count') ||
            $elem.css('-moz-animation-iteration-count') ||
            $elem.css('-o-animation-iteration-count') ||
            $elem.css('-ms-animation-iteration-count') ||
            '1';

        var max;
        var len;
        var num;
        var i;

        duration = duration.split(', ');
        delay = delay.split(', ');
        iterationCount = iterationCount.split(', ');

        // The 'duration' size is the same as the 'delay' size
        for (i = 0, len = duration.length, max = Number.NEGATIVE_INFINITY; i < len; i++) {
            num = parseFloat(duration[i]) * parseInt(iterationCount[i], 10) + parseFloat(delay[i]);

            if (num > max) {
                max = num;
            }
        }

        return max;
    }

    /**
     * Returns a scrollbar width
     * @private
     * @returns {Number}
     */
    function getScrollbarWidth() {
        if ($(document.body).height() <= $(window).height()) {
            return 0;
        }

        var outer = document.createElement('div');
        var inner = document.createElement('div');
        var widthNoScroll;
        var widthWithScroll;

        outer.style.visibility = 'hidden';
        outer.style.width = '100px';
        document.body.appendChild(outer);

        widthNoScroll = outer.offsetWidth;

        // Force scrollbars
        outer.style.overflow = 'scroll';

        // Add inner div
        inner.style.width = '100%';
        outer.appendChild(inner);

        widthWithScroll = inner.offsetWidth;

        // Remove divs
        outer.parentNode.removeChild(outer);

        return widthNoScroll - widthWithScroll;
    }

    /**
     * Locks the screen
     * @private
     */
    function lockScreen() {
        if (IS_IOS) {
            return;
        }

        var $html = $('html');
        var lockedClass = namespacify('is-locked');
        var paddingRight;
        var $body;

        if (!$html.hasClass(lockedClass)) {
            $body = $(document.body);

            // Zepto does not support '-=', '+=' in the `css` method
            paddingRight = parseInt($body.css('padding-right'), 10) + getScrollbarWidth();

            $body.css('padding-right', paddingRight + 'px');
            $html.addClass(lockedClass);
        }
    }

    /**
     * Unlocks the screen
     * @private
     */
    function unlockScreen() {
        if (IS_IOS) {
            return;
        }

        var $html = $('html');
        var lockedClass = namespacify('is-locked');
        var paddingRight;
        var $body;

        if ($html.hasClass(lockedClass)) {
            $body = $(document.body);

            // Zepto does not support '-=', '+=' in the `css` method
            paddingRight = parseInt($body.css('padding-right'), 10) - getScrollbarWidth();

            $body.css('padding-right', paddingRight + 'px');
            $html.removeClass(lockedClass);
        }
    }

    /**
     * Sets a state for an instance
     * @private
     * @param {Remodal} instance
     * @param {STATES} state
     * @param {Boolean} isSilent If true, Remodal does not trigger events
     * @param {String} Reason of a state change.
     */
    function setState(instance, state, isSilent, reason) {

        var newState = namespacify('is', state);
        var allStates = [namespacify('is', STATES.CLOSING),
            namespacify('is', STATES.OPENING),
            namespacify('is', STATES.CLOSED),
            namespacify('is', STATES.OPENED)].join(' ');

        instance.$bg
            .removeClass(allStates)
            .addClass(newState);

        instance.$overlay
            .removeClass(allStates)
            .addClass(newState);

        instance.$wrapper
            .removeClass(allStates)
            .addClass(newState);

        instance.$modal
            .removeClass(allStates)
            .addClass(newState);

        instance.state = state;
        !isSilent && instance.$modal.trigger({
            type: state,
            reason: reason
        }, [{reason: reason}]);
    }

    /**
     * Synchronizes with the animation
     * @param {Function} doBeforeAnimation
     * @param {Function} doAfterAnimation
     * @param {Remodal} instance
     */
    function syncWithAnimation(doBeforeAnimation, doAfterAnimation, instance) {
        var runningAnimationsCount = 0;

        var handleAnimationStart = function (e) {
            if (e.target !== this) {
                return;
            }

            runningAnimationsCount++;
        };

        var handleAnimationEnd = function (e) {
            if (e.target !== this) {
                return;
            }

            if (--runningAnimationsCount === 0) {

                // Remove event listeners
                $.each(['$bg', '$overlay', '$wrapper', '$modal'], function (index, elemName) {
                    instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS);
                });

                doAfterAnimation();
            }
        };

        $.each(['$bg', '$overlay', '$wrapper', '$modal'], function (index, elemName) {
            instance[elemName]
                .on(ANIMATIONSTART_EVENTS, handleAnimationStart)
                .on(ANIMATIONEND_EVENTS, handleAnimationEnd);
        });

        doBeforeAnimation();

        // If the animation is not supported by a browser or its duration is 0
        if (
            getAnimationDuration(instance.$bg) === 0 &&
            getAnimationDuration(instance.$overlay) === 0 &&
            getAnimationDuration(instance.$wrapper) === 0 &&
            getAnimationDuration(instance.$modal) === 0
        ) {

            // Remove event listeners
            $.each(['$bg', '$overlay', '$wrapper', '$modal'], function (index, elemName) {
                instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS);
            });

            doAfterAnimation();
        }
    }

    /**
     * Closes immediately
     * @private
     * @param {Remodal} instance
     */
    function halt(instance) {
        if (instance.state === STATES.CLOSED) {
            return;
        }

        $.each(['$bg', '$overlay', '$wrapper', '$modal'], function (index, elemName) {
            instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS);
        });

        instance.$bg.removeClass(instance.settings.modifier);
        instance.$overlay.removeClass(instance.settings.modifier).hide();
        instance.$wrapper.hide();
        unlockScreen();
        setState(instance, STATES.CLOSED, true);
    }

    /**
     * Parses a string with options
     * @private
     * @param str
     * @returns {Object}
     */
    function parseOptions(str) {
        var obj = {};
        var arr;
        var len;
        var val;
        var i;

        // Remove spaces before and after delimiters
        str = str.replace(/\s*:\s*/g, ':').replace(/\s*,\s*/g, ',');

        // Parse a string
        arr = str.split(',');
        for (i = 0, len = arr.length; i < len; i++) {
            arr[i] = arr[i].split(':');
            val = arr[i][1];

            // Convert a string value if it is like a boolean
            if (typeof val === 'string' || val instanceof String) {
                val = val === 'true' || (val === 'false' ? false : val);
            }

            // Convert a string value if it is like a number
            if (typeof val === 'string' || val instanceof String) {
                val = !isNaN(val) ? +val : val;
            }

            obj[arr[i][0]] = val;
        }

        return obj;
    }

    /**
     * Generates a string separated by dashes and prefixed with NAMESPACE
     * @private
     * @param {...String}
     * @returns {String}
     */
    function namespacify() {
        var result = NAMESPACE;

        for (var i = 0; i < arguments.length; ++i) {
            result += '-' + arguments[i];
        }

        return result;
    }

    /**
     * Handles the hashchange event
     * @private
     * @listens hashchange
     */
    function handleHashChangeEvent() {
        var id = location.hash.replace('#', '');
        var instance;
        var $elem;

        if (!id) {

            // Check if we have currently opened modal and animation was completed
            if (current && current.state === STATES.OPENED && current.settings.hashTracking) {
                current.close();
            }
        } else {

            // Catch syntax error if your hash is bad
            try {
                $elem = $(
                    '[data-' + PLUGIN_NAME + '-id="' + id + '"]'
                );
            } catch (err) {
            }

            if ($elem && $elem.length) {
                instance = $[PLUGIN_NAME].lookup[$elem.data(PLUGIN_NAME)];

                if (instance && instance.settings.hashTracking) {
                    instance.open();
                }
            }

        }
    }

    /**
     * Remodal constructor
     * @constructor
     * @param {jQuery} $modal
     * @param {Object} options
     */
    function Remodal($modal, options) {
        var $body = $(document.body);
        var $appendTo = $body;
        var remodal = this;

        remodal.settings = $.extend({}, DEFAULTS, options);
        remodal.index = $[PLUGIN_NAME].lookup.push(remodal) - 1;
        remodal.state = STATES.CLOSED;

        remodal.$overlay = $('.' + namespacify('overlay'));

        if (remodal.settings.appendTo !== null && remodal.settings.appendTo.length) {
            $appendTo = $(remodal.settings.appendTo);
        }

        if (!remodal.$overlay.length) {
            remodal.$overlay = $('<div>').addClass(namespacify('overlay') + ' ' + namespacify('is', STATES.CLOSED)).hide();
            $appendTo.append(remodal.$overlay);
        }

        remodal.$bg = $('.' + namespacify('bg')).addClass(namespacify('is', STATES.CLOSED));

        remodal.$modal = $modal
            .addClass(
                NAMESPACE + ' ' +
                namespacify('is-initialized') + ' ' +
                remodal.settings.modifier + ' ' +
                namespacify('is', STATES.CLOSED))
            .attr('tabindex', '-1');

        remodal.$wrapper = $('<div>')
            .addClass(
                namespacify('wrapper') + ' ' +
                remodal.settings.modifier + ' ' +
                namespacify('is', STATES.CLOSED))
            .hide()
            .append(remodal.$modal);
        $appendTo.append(remodal.$wrapper);

        // Add the event listener for the close button
        remodal.$wrapper.on('click.' + NAMESPACE, '[data-' + PLUGIN_NAME + '-action="close"]', function (e) {
            e.preventDefault();

            remodal.close();
        });

        // Add the event listener for the cancel button
        remodal.$wrapper.on('click.' + NAMESPACE, '[data-' + PLUGIN_NAME + '-action="cancel"]', function (e) {
            e.preventDefault();

            remodal.$modal.trigger(STATE_CHANGE_REASONS.CANCELLATION);

            if (remodal.settings.closeOnCancel) {
                remodal.close(STATE_CHANGE_REASONS.CANCELLATION);
            }
        });

        // Add the event listener for the confirm button
        remodal.$wrapper.on('click.' + NAMESPACE, '[data-' + PLUGIN_NAME + '-action="confirm"]', function (e) {
            e.preventDefault();

            remodal.$modal.trigger(STATE_CHANGE_REASONS.CONFIRMATION);

            if (remodal.settings.closeOnConfirm) {
                remodal.close(STATE_CHANGE_REASONS.CONFIRMATION);
            }
        });

        // Add the event listener for the overlay
        remodal.$wrapper.on('click.' + NAMESPACE, function (e) {
            var $target = $(e.target);

            if (!$target.hasClass(namespacify('wrapper'))) {
                return;
            }

            if (remodal.settings.closeOnOutsideClick) {
                remodal.close();
            }
        });
    }

    /**
     * Opens a modal window
     * @public
     */
    Remodal.prototype.open = function () {
        var remodal = this;
        var id;

        // Check if the animation was completed
        if (remodal.state === STATES.OPENING || remodal.state === STATES.CLOSING) {
            return;
        }

        id = remodal.$modal.attr('data-' + PLUGIN_NAME + '-id');

        if (id && remodal.settings.hashTracking) {
            scrollTop = $(window).scrollTop();
            location.hash = id;
        }

        if (current && current !== remodal) {
            halt(current);
        }

        current = remodal;
        lockScreen();
        remodal.$bg.addClass(remodal.settings.modifier);
        remodal.$overlay.addClass(remodal.settings.modifier).show();
        remodal.$wrapper.show().scrollTop(0);
        remodal.$modal.focus();

        syncWithAnimation(
            function () {
                setState(remodal, STATES.OPENING);
            },

            function () {
                setState(remodal, STATES.OPENED);
            },

            remodal);
    };

    /**
     * Closes a modal window
     * @public
     * @param {String} reason
     */
    Remodal.prototype.close = function (reason) {
        var remodal = this;

        // Check if the animation was completed
        if (remodal.state === STATES.OPENING || remodal.state === STATES.CLOSING) {
            return;
        }

        if (
            remodal.settings.hashTracking &&
            remodal.$modal.attr('data-' + PLUGIN_NAME + '-id') === location.hash.substr(1)
        ) {
            location.hash = '';
            $(window).scrollTop(scrollTop);
        }

        syncWithAnimation(
            function () {
                setState(remodal, STATES.CLOSING, false, reason);
            },

            function () {
                remodal.$bg.removeClass(remodal.settings.modifier);
                remodal.$overlay.removeClass(remodal.settings.modifier).hide();
                remodal.$wrapper.hide();
                unlockScreen();

                setState(remodal, STATES.CLOSED, false, reason);
            },

            remodal);
    };

    /**
     * Returns a current state of a modal
     * @public
     * @returns {STATES}
     */
    Remodal.prototype.getState = function () {
        return this.state;
    };

    /**
     * Destroys a modal
     * @public
     */
    Remodal.prototype.destroy = function () {
        var lookup = $[PLUGIN_NAME].lookup;
        var instanceCount;

        halt(this);
        this.$wrapper.remove();

        delete lookup[this.index];
        instanceCount = $.grep(lookup, function (instance) {
            return !!instance;
        }).length;

        if (instanceCount === 0) {
            this.$overlay.remove();
            this.$bg.removeClass(
                namespacify('is', STATES.CLOSING) + ' ' +
                namespacify('is', STATES.OPENING) + ' ' +
                namespacify('is', STATES.CLOSED) + ' ' +
                namespacify('is', STATES.OPENED));
        }
    };

    /**
     * Special plugin object for instances
     * @public
     * @type {Object}
     */
    $[PLUGIN_NAME] = {
        lookup: []
    };

    /**
     * Plugin constructor
     * @constructor
     * @param {Object} options
     * @returns {JQuery}
     */
    $.fn[PLUGIN_NAME] = function (opts) {
        var instance;
        var $elem;

        this.each(function (index, elem) {
            $elem = $(elem);

            if ($elem.data(PLUGIN_NAME) == null) {
                instance = new Remodal($elem, opts);
                $elem.data(PLUGIN_NAME, instance.index);

                if (
                    instance.settings.hashTracking &&
                    $elem.attr('data-' + PLUGIN_NAME + '-id') === location.hash.substr(1)
                ) {
                    instance.open();
                }
            } else {
                instance = $[PLUGIN_NAME].lookup[$elem.data(PLUGIN_NAME)];
            }
        });

        return instance;
    };

    $(document).ready(function () {

        // data-remodal-target opens a modal window with the special Id
        $(document).on('click', '[data-' + PLUGIN_NAME + '-target]', function (e) {
            e.preventDefault();

            var elem = e.currentTarget;
            var id = elem.getAttribute('data-' + PLUGIN_NAME + '-target');
            var $target = $('[data-' + PLUGIN_NAME + '-id="' + id + '"]');

            $[PLUGIN_NAME].lookup[$target.data(PLUGIN_NAME)].open();
        });

        // Auto initialization of modal windows
        // They should have the 'remodal' class attribute
        // Also you can write the `data-remodal-options` attribute to pass params into the modal
        $(document).find('.' + NAMESPACE).each(function (i, container) {
            var $container = $(container);
            var options = $container.data(PLUGIN_NAME + '-options');

            if (!options) {
                options = {};
            } else if (typeof options === 'string' || options instanceof String) {
                options = parseOptions(options);
            }

            $container[PLUGIN_NAME](options);
        });

        // Handles the keydown event
        $(document).on('keydown.' + NAMESPACE, function (e) {
            if (current && current.settings.closeOnEscape && current.state === STATES.OPENED && e.keyCode === 27) {
                current.close();
            }
        });

        // Handles the hashchange event
        $(window).on('hashchange.' + NAMESPACE, handleHashChangeEvent);
    });
});
